﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="PersistentStorage.cs" company="XamlNinja">
//   2011 Richard Griffin and Ollie Riches
// </copyright>
// <summary>
//   Persistence storage - uses isolated storage settings to permanently persist settings.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace WP7Contrib.Services.Storage
{
    using System;
    using System.Collections.Generic;
    using System.IO.IsolatedStorage;
    using System.Linq.Expressions;
    using Logging;
    using Common.Helpers;
    using global::Serialization;

    /// <summary>
    /// Persistence storage - uses isolated storage settings to permanently persist settings.
    /// </summary>
    public sealed class PersistentStorage : IStorage
    {
        private const string FailedToWrite = "Failed to write to persistence storage.";

        private const string FailedToRead = "Failed to read from persistence storage.";

        private const string FailedToFlush = "Failed to flush to persistence storage";

        private const string FailedToClear = "Failed to clear persistence storage";

        /// <summary>
        /// The logger used to capture diagnostics information.
        /// </summary>
        private readonly ILog log;

        /// <summary>
        ///  The context for the storage, e.g. view model name
        /// </summary>
        private readonly string storageName;

        /// <summary>
        /// The application state.
        /// </summary>
        private readonly IsolatedStorageSettings applicationState;

        /// <summary>
        /// The StateSync object to make persisting or reviving the state thread safe.
        /// </summary>
        private readonly static object StateSync = new object();

        /// <summary>
        /// The in memory representation of the persistence store.
        /// </summary>
        private IDictionary<string, object> store;

        /// <summary>
        /// Flag indicating when a write to the application state is required.
        /// </summary>
        private bool persistRequired;

        /// <summary>
        /// Flag indicating if the persistence storage has been loaded from application state.
        /// </summary>
        private bool loaded;

        /// <summary>
        /// Initializes a new instance of the <see cref="PersistentStorage"/> class.
        /// </summary>
        /// <param name="storageName">
        /// The storage name context.
        /// </param>
        public PersistentStorage(string storageName) : this(new DebugLog(), storageName)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="PersistentStorage"/> class.
        /// </summary>
        /// <param name="log">
        /// The logger used to capture diagnostics information.
        /// </param>
        /// <param name="storageName">
        /// The storage name context.
        /// </param>
        public PersistentStorage(ILog log, string storageName)
        {
            this.log = log;

            this.applicationState = IsolatedStorageSettings.ApplicationSettings;
            this.storageName = storageName.ToLowerInvariant();
        }

        /// <summary>
        /// The dispose.
        /// </summary>
        public void Dispose()
        {
            if (this.loaded)
            {
                this.PersistState();
            }
        }

        /// <summary>
        /// Gets the name context for the current storage.
        /// </summary>
        public string Name
        {
            get { return this.storageName; }
        }

        /// <summary>
        /// Writes a value to storage.
        /// </summary>
        /// <param name="name">
        /// The name of the name value pair.
        /// </param>
        /// <param name="value">
        /// The value of the name value pair.
        /// </param>
        /// <typeparam name="T">
        /// The type of the value of the name value pair.
        /// </typeparam>
        /// <returns>
        /// Returns the instance of the storage - fluent interface style.
        /// </returns>
        public IStorage Write<T>(string name, T value)
        {
            try
            {
                this.log.Write("PersistentStorage: Write, name - '{0}', value - '{1}'", name, value);

                this.WriteImpl(name, value);
                this.persistRequired = true;

                return this;
            }
            catch (Exception exn)
            {
                throw new ServiceException(FailedToWrite, exn);
            }
        }

        /// <summary>
        /// Writes a value to storage.
        /// </summary>
        /// <param name="expression">
        /// Expression which name represents the name of the name value pair.
        /// </param>
        /// <param name="value">
        /// The value of the name value pair.
        /// </param>
        /// <typeparam name="T">
        /// The type of the value of the name value pair.
        /// </typeparam>
        /// <returns>
        /// Returns the instance of the storage - fluent interface style.
        /// </returns>
        public IStorage Write<T>(Expression<System.Func<object>> expression, T value)
        {
            return Write(ExpressionHelper.ExpressionName(expression), value);
        }

        /// <summary>
        /// Reads a value from storage using the name of an existing name value pair.
        /// </summary>
        /// <param name="name">
        /// The name of the name value pair.
        /// </param>
        /// <typeparam name="T">
        /// The type of the value of the name value pair.
        /// </typeparam>
        /// <returns>
        /// Returns the value or null.
        /// </returns>
        public T Read<T>(string name)
        {
            try
            {
                this.log.Write("PersistentStorage: Read, name - '{0}'", name);

                return this.ReadImpl<T>(name);
            }
            catch (Exception exn)
            {
                throw new ServiceException(FailedToRead, exn);
            }
        }

        /// <summary>
        /// Reads a value from storage using the name of an existing name value pair.
        /// </summary>
        /// <param name="expression">
        /// Expression which name represents the name of the name value pair.
        /// </param>
        /// <typeparam name="T">
        /// The type of the value of the name value pair.
        /// </typeparam>
        /// <returns>
        /// Returns the value or null.
        /// </returns>
        public T Read<T>(Expression<Func<object>> expression)
        {
            var name = ExpressionHelper.ExpressionName(expression);
            return this.Read<T>(name);
        }

        /// <summary>
        /// Flushes any pending storage requests.
        /// </summary>
        /// <returns>
        /// Returns the instance of the storage - fluent interface style.
        /// </returns>
        public IStorage Flush()
        {
            try
            {
                this.log.Write("PersistentStorage: Flush...");

                this.PersistState();

                return this;
            }
            catch (Exception exn)
            {
                throw new ServiceException(FailedToFlush, exn);
            }
        }

        /// <summary>
        /// Clears the storage.
        /// </summary>
        /// <returns>
        /// Returns the instance of the storage - fluent interface style.
        /// </returns>
        public IStorage Clear()
        {
            try
            {
                this.log.Write("PersistentStorage: Clear...");

                if (!this.loaded)
                {
                    this.ReviveState();
                }

                this.store.Clear();
                this.persistRequired = true;

                return this;
            }
            catch (Exception exn)
            {
                throw new ServiceException(FailedToClear, exn);
            }
        }

        /// <summary>
        /// Write implementation, if the storage has never previously been loaded it is first loaded, the name value pair is then
        /// written to the in memory version. 
        /// </summary>
        /// <param name="name">
        /// The name of the name value pair to be stored.
        /// </param>
        /// <param name="value">
        /// The value of the name value pair to be stored.
        /// </param>
        private void WriteImpl(string name, object value)
        {
            if (!this.loaded)
            {
                this.ReviveState();
            }

            var key = name.ToLowerInvariant();

            if (this.store.ContainsKey(key))
            {
                this.store[key] = value;
            }
            else
            {
                this.store.Add(key, value);
            }
        }

        /// <summary>
        /// Read implementation, if the storage has never previously been loaded it is first loaded, the name value pair is then
        /// read from the in memory dictionary. 
        /// </summary>
        /// <param name="name">
        /// The name of the name value pair to be stored.
        /// </param>
        /// <typeparam name="T">The type of the value of the name value pair.
        /// </typeparam>
        /// <returns>
        /// Returns the value or null.
        /// </returns>
        private T ReadImpl<T>(string name)
        {
            if (!this.loaded)
            {
                this.ReviveState();
            }

            var key = name.ToLowerInvariant();

            return this.store.ContainsKey(key) ? (T)this.store[key] : default(T);
        }

        /// <summary>
        /// Persists the in memory store to application state, we serialize the in memory store using the 
        /// SilverlightSerializer.
        /// </summary>
        private void PersistState()
        {
            if (!this.persistRequired || !this.loaded)
            {
                return;
            }

            try
            {
                this.log.Write("PersistentStorage: PersistState...");

                lock (StateSync)
                {
                    var data = SilverlightSerializer.Serialize(this.store);

                    if (this.applicationState.Contains(this.storageName))
                    {
                        this.applicationState[this.storageName] = data;
                    }
                    else
                    {
                        this.applicationState.Add(this.storageName, data);
                    }

                    this.applicationState.Save();
                }
            }
            catch (Exception exn)
            {
                this.log.Write("PersistentStorage: Failed to persist persistent store, message - '{0}'.", exn.Message);
            }
        }

        /// <summary>
        /// Revives the in memory store from application state, we deserialize the value read from application state
        /// using the SilverlightSerializer. this can only happen once per instance.
        /// </summary>
        private void ReviveState()
        {
            if (this.loaded)
            {
                return;
            }

            lock (StateSync)
            {
                if (this.loaded)
                {
                    return;
                }

                if (this.applicationState.Contains(this.storageName))
                {
                    try
                    {
                        var data = (byte[])this.applicationState[this.storageName];

                        this.store = SilverlightSerializer.Deserialize<IDictionary<string, object>>(data);
                        this.log.Write("PersistentStorage: Revived persistent store, name - '{0}'.", this.storageName);
                    }
                    catch (Exception exn)
                    {
                        this.log.Write("PersistentStorage: Failed to revive persistent store, message - '{0}'.", exn.Message);
                        this.store = new Dictionary<string, object>();
                        this.persistRequired = true;
                    }
                }
                else
                {
                    this.store = new Dictionary<string, object>();
                    this.persistRequired = true;
                }

                this.loaded = true;
            }
        }
    }
}